/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.seppiko.pigeon.utils;

import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.seppiko.commons.mail.JakartaMailReceiver;
import org.seppiko.commons.mail.JakartaMailSender;
import org.seppiko.commons.mail.MailMessage;
import org.seppiko.commons.utils.ObjectUtil;
import org.seppiko.commons.utils.StringUtil;
import org.seppiko.pigeon.configuration.PigeonConfiguration;
import org.seppiko.pigeon.exceptions.MailAddressFormatException;
import org.seppiko.pigeon.exceptions.PigeonCheckException;
import org.seppiko.pigeon.models.ImapConfigEntity;
import org.seppiko.pigeon.models.MailConfigEntity;
import org.seppiko.pigeon.models.MailEntity;
import org.seppiko.pigeon.models.SmtpConfigEntity;

/**
 * Mail Util
 *
 * @author Leonard Woo
 */
@Slf4j
public class MailUtil {

  private static PigeonConfiguration config = PigeonConfiguration.getInstance();

  private static MailConfigEntity mailConfig;

  private static int connectionTimeout;
  private static int timeout;
  private static int writeTimeout;

  static {
    mailConfig = config.getMailConfig();

    connectionTimeout = mailConfig.getConnectionTimeout();
    timeout = mailConfig.getTimeout();
    writeTimeout =  mailConfig.getWriteTimeout();

  }

  /**
   * Sender mail
   *
   * @param smtpConfig SMTP config
   * @return Jakarta Mail sender
   */
  public static JakartaMailSender getMailSender(SmtpConfigEntity smtpConfig) {
    Properties props = new Properties();
    props.setProperty("mail.encoding", "UTF-8");

    String protocol = smtpConfig.getSmtpTlsEnable()? "smtps": "smtp";
    props.setProperty("mail.transport.protocol", protocol);
    props.putAll( setConfig(protocol, smtpConfig.getHost(), smtpConfig.getPort(),
        smtpConfig.getSmtpAuth()) );
    props.putAll( setTls("smtp", smtpConfig.getSmtpTlsEnable(),
        smtpConfig.getSmtpStartTlsEnable(), smtpConfig.getSmtpStartTlsRequired()) );

    if (mailConfig.isProxyEnable()) {
      try {
        props.putAll(setProxy(protocol, mailConfig.getProxyUrl()));
      } catch (URISyntaxException ex) {
        log.info("Proxy set failed", ex);
      }
    }

    Session session = Session.getInstance(props, null);
    JakartaMailSender mailSender = new JakartaMailSender(session);

    mailSender.setUser(smtpConfig.getUsername(), smtpConfig.getPassword());

    return mailSender;
  }

  /**
   * Receiver mail to MailEntity list
   *
   * @param imapConfig IMAP config
   * @return MailEntity list if null is failed
   */
  public static ArrayList<MailEntity> mailReceiver(ImapConfigEntity imapConfig) {
    Properties props = new Properties();
    props.setProperty("mail.encoding", "UTF-8");

    String protocol = imapConfig.getImapTlsEnable()? "imaps": "imap";
    props.setProperty("mail.store.protocol", protocol);
    props.putAll( setConfig(protocol, imapConfig.getHost(), imapConfig.getPort(),
        imapConfig.getImapAuth()) );
    props.putAll( setTls("imap", imapConfig.getImapTlsEnable(),
        imapConfig.getImapStartTlsEnable(), imapConfig.getImapStartTlsRequired()) );

    if (mailConfig.isProxyEnable()) {
      try {
        props.putAll(setProxy(protocol, mailConfig.getProxyUrl()));
      } catch (URISyntaxException ex) {
        log.info("Proxy set failed", ex);
      }
    }

    Session session = Session.getInstance(props, null);
    JakartaMailReceiver mailReceiver = new JakartaMailReceiver(session);

    mailReceiver.setUser(imapConfig.getUsername(), imapConfig.getPassword());

    try {
      ArrayList<MailEntity> mails = new ArrayList<>();
      ArrayList<MailMessage> messages = mailReceiver.receive("INBOX");
      messages.forEach(message -> {
        MailEntity mail = new MailEntity();
        mail.setFrom(message.getFrom().getAddress());
        mail.setReplayTo(message.getReplyTo().getAddress());
        mail.setSubject(message.getSubject());
        mail.setText(message.getText());
        mails.add(mail);
      });
      return mails;
    } catch (IllegalAccessException | IOException | MessagingException ex) {
      log.warn("", ex);
    }
    return null;
  }


  /**
   * Set tls properties
   *
   * @param protocol mail protocol
   * @param tlsEnable mail ssl enable
   * @param startTlsEnable mail starttls enable
   * @param startTlsRequired mail starttls required
   * @return properties map
   */
  private static LinkedHashMap<String, String> setTls(String protocol, boolean tlsEnable, boolean startTlsEnable, boolean startTlsRequired){
    LinkedHashMap<String, String> props = new LinkedHashMap<>();
    props.put("mail." + protocol + ".ssl.enable", tlsEnable + "");
    props.put("mail." + protocol + ".starttls.enable", startTlsEnable + "");
    props.put("mail." + protocol + ".starttls.required", startTlsRequired + "");

    return props;
  }

  /**
   * set basic properties
   *
   * @param protocol mail protocol
   * @param host mail host
   * @param port mail port
   * @param auth mail auth
   * @return properties map
   */
  private static LinkedHashMap<String, String> setConfig(String protocol, String host, int port, boolean auth) {
    LinkedHashMap<String, String> props = new LinkedHashMap<>();
    props.put("mail." + protocol + ".host", host);
    props.put("mail." + protocol + ".port", port + "");
    props.put("mail." + protocol + ".auth", auth + "");

    props.put("mail." + protocol + ".connectiontimeout", connectionTimeout + "");
    props.put("mail." + protocol + ".timeout", timeout + "");
    props.put("mail." + protocol + ".writetimeout", writeTimeout + "");

    return props;
  }

  /**
   * Set proxy properties
   * No auth
   *
   * @param protocol smtp OR imap
   * @param proxyUrl http://localhost:8000 OR socks://localhost:1080 OR socks5://localhost:1080
   * @return Properties Map
   * @throws URISyntaxException URI error
   */
  private static LinkedHashMap<String, String> setProxy(String protocol, String proxyUrl)
      throws URISyntaxException {
    LinkedHashMap<String, String> props = new LinkedHashMap<>();
    URI uri = new URI(proxyUrl);
    String scheme = uri.getScheme();
    if (scheme.equals("http")) {
      scheme = "proxy";
    } else if (scheme.matches("socks[5]?")) {
      scheme = "socks";
    } else {
      throw new URISyntaxException("Not right proxy uri", "");
    }
    props.put("mail." + protocol + "." + scheme + ".host", uri.getHost());
    props.put("mail." + protocol + "." + scheme + ".port", uri.getPort() + "");

    return props;
  }

  /**
   * Config checker
   *
   * @param host mail host
   * @param port mail port
   * @param username mail username
   * @param password mail password
   * @param auth mail server auth
   * @param tlsEnable mail server tls
   * @param startTlsEnable mail server start tls
   * @param startTlsRequired mail server start tls required
   * @throws PigeonCheckException Data checker failed
   */
  public static void checker(String host, Integer port, String username, String password,
      Boolean auth, Boolean tlsEnable, Boolean startTlsEnable, Boolean startTlsRequired) throws PigeonCheckException {
    if (ObjectUtil.isNull(hostCheck(host))) {
      throw new PigeonCheckException("host must NOT be null and valid");
    }
    if (port == null || portCheck(port)) {
      throw new PigeonCheckException("port must NOT be null and between 0 and 65535");
    }
    if (auth == null) {
      throw new PigeonCheckException("auth must NOT be null");
    }
    if (!auth && !StringUtil.hasText(username) && !StringUtil.hasText(password)) {
      throw new PigeonCheckException("the username and password must NOT be null when auth is true");
    }
    if (tlsEnable == null) {
      throw new PigeonCheckException("tlsEnable must NOT be null");
    }
    if (startTlsEnable == null || startTlsRequired == null) {
      throw new PigeonCheckException("startTlsEnable and startTlsRequired must NOT be null");
    }
  }

  /**
   * Host checker
   *
   * @param host hostname
   * @return DNS lookup if null is failed
   */
  public static InetAddress hostCheck(String host) {
    if (StringUtil.isEmpty(host)) {
      return null;
    }
    try {
      return InetAddress.getByName(host);
    } catch (UnknownHostException e) {
      return null;
    }
  }

  /**
   * Port checker
   *
   * @param port port
   * @return if true port is between 0 and 65535
   */
  public static boolean portCheck(int port) {
    return port > 0 && port < 0xFFFF;
  }

  /**
   * Parser addresses to InternetAddresses
   *
   * @param addresses mail addresses
   * @return InternetAddresses
   * @throws AddressException mail address parser failed
   */
  public static InternetAddress[] parser(String[] addresses) throws AddressException {
    InternetAddress[] IAs = new InternetAddress[addresses.length];
    for (int i = 0; i < addresses.length; i++) {
      IAs[i] = parserFrist(addresses[i]);
    }
    return IAs;
  }

  private static InternetAddress parserFrist(String address) throws AddressException {
    return InternetAddress.parse(address)[0];
  }

  /**
   * Obfuscate email addresses
   *
   * @param adds mail addresses
   * @param obfsUsername obfuscate username
   * @param obfsDomain obfuscate domain without tld
   * @return obfuscated addresses
   * @throws MailAddressFormatException email address verification failed
   */
  public static String[] hideAdds(String[] adds, boolean obfsUsername, boolean obfsDomain) throws MailAddressFormatException {
    if (adds == null || adds.length == 0) {
      return adds;
    }
    String[] addResult = new String[adds.length];
    for (int i = 0; i < adds.length; i++) {
      addResult[i] = hideAdd(adds[i], obfsUsername, obfsDomain);
    }
    return addResult;
  }

  private static final String OBFS = "*".repeat(3);

  /**
   * Obfuscate email address
   *
   * @param add mail address
   * @param obfsUsername obfuscate username
   * @param obfsDomain obfuscate domain without tld
   * @return obfuscated address
   * @throws MailAddressFormatException email address verification failed
   */
  public static String hideAdd(String add, boolean obfsUsername, boolean obfsDomain) throws MailAddressFormatException {
    if (add == null || "".equals(add)) {
      return add;
    }

    add = mailVaild(add);

    String[] mailAddr = add.split("@");
    String username = mailAddr[0];
    String domain = mailAddr[1];

    if (obfsUsername) {
      if (username.length() > 1) {
        username = username.charAt(0) + OBFS;
      } else {
        username = username + OBFS;
      }
    }

    if (obfsDomain) {
      String tld = domain.substring(domain.lastIndexOf("."));
      domain = domain.charAt(0) + OBFS + tld;
    }

    return username + "@" + domain;
  }

  /**
   * Mail address resolution and verification
   *
   * @param mailAddress mail address
   * @return jakarta mail address
   * @throws MailAddressFormatException email address resolution failed
   */
  private static String mailVaild(String mailAddress) throws MailAddressFormatException {
    try {
      return parserFrist(mailAddress).getAddress();
    } catch (AddressException ex) {
      throw new MailAddressFormatException(mailAddress);
    }
  }
}
